using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace RpcCodegen {
	[Generator]
	public class RpcAttributeGenerator : ISourceGenerator {
		private const string _rpcAttributeText = @"
using System;
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
class ClientRPCAttribute : Attribute {
    public byte MethodIndex { get; }

    public ClientRPCAttribute(byte methodIndex) {
		MethodIndex = methodIndex;
    }
}
";
		private const string _syncVarAttributeText = @"
using System;
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
class SyncVarAttribute : Attribute {
    public byte VarIndex { get; }

    public SyncVarAttribute(byte varIndex) {
		VarIndex = varIndex;
    }
}
";

		public void Initialize(GeneratorInitializationContext context) {
			context.RegisterForPostInitialization
				(i => {
					i.AddSource("SyncVarAttribute_g.cs", _syncVarAttributeText);
					i.AddSource("ClientRPCAttribute_g.cs", _rpcAttributeText);
				});
			context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
		}

		class EntityClassInfo {
			public string ClassName;
			public List<RPCMethodInfo> Methods  = new List<RPCMethodInfo>();
			public List<SyncVarInfo>   SyncVars = new List<SyncVarInfo>();
		}

		public void Execute(GeneratorExecutionContext context) {
			if ( !(context.SyntaxContextReceiver is SyntaxReceiver receiver) ) {
				return;
			}

			var debugSb = new StringBuilder();
			debugSb.AppendLine("/*");

			try {
				var entityBaseClass = context.Compilation.GetTypeByMetadataName("Game.Networking.Entity");

				var methodGroups = new Dictionary<string, EntityClassInfo>();
				foreach ( var method in receiver.MethodInfos ) {
					var className = method.ClassName.Identifier.ToString();
					if ( !methodGroups.ContainsKey(className) ) {
						methodGroups[className] = new EntityClassInfo();
						methodGroups[className].ClassName = className;
					}
					methodGroups[className].Methods.Add(method);
					debugSb.AppendLine($"Method: {method.AttributeValue} {method.ClassName.Identifier} {method.Namespace} {method.Parameters}");
				}

				foreach ( var syncVar in receiver.SyncVarInfos ) {
					var className = syncVar.ClassName.Identifier.ToString();
					if ( !methodGroups.ContainsKey(className) ) {
						methodGroups[className] = new EntityClassInfo();
						methodGroups[className].ClassName = className;
					}
					methodGroups[className].SyncVars.Add(syncVar);
					debugSb.AppendLine($"SyncVar: {syncVar.VarName} {syncVar.AttributeValue} {syncVar.ClassName.Identifier} {syncVar.Namespace} {syncVar.VarType}");
				}

				foreach ( var kvp in methodGroups ) {
					if ( kvp.Value.Methods.Count > 0 || kvp.Value.SyncVars.Count > 0 ) {
						var classSource = ProcessEntityClass(kvp.Key, kvp.Value.Methods, kvp.Value.SyncVars);
						context.AddSource($"{kvp.Key}_RPCHelper_g.cs", SourceText.From(classSource, Encoding.UTF8));
					}
				}
				debugSb.AppendLine($"MethodGroups: {methodGroups.Count}");
			} catch( Exception ex ) {
				debugSb.Append(ex.ToString());
			}
			debugSb.AppendLine("*/");
			//context.AddSource("Test_Debug_g.cs", SourceText.From(debugSb.ToString(), Encoding.UTF8));
		}

		string ProcessEntityClass(string className, List<RPCMethodInfo> methods, List<SyncVarInfo> syncVars) {
			var source = new StringBuilder(@"#pragma warning disable CS0162 // Unreachable code
#pragma warning disable CS0105 // Double using statement
#pragma warning disable CS0219 // Variable assigned but never used
");
			AddUsings(source, methods, syncVars);
			source.AppendLine($@"
using System;
using Game.Networking;
using Game.Networking.Server;

namespace {GetNamespace(methods, syncVars)} {{
	public partial class {className} {{");

			foreach ( var sv in  syncVars ) {
				AddSyncVarCompareField(source, sv);
			}
			AddSyncVarInit(source, syncVars);
			AddSyncVarCheckMethod(source, syncVars);
			AddSyncVarApplyMethod(source, syncVars);
			AddServerRpcMethods(source, methods);
			source.AppendLine();
			AddClientRpcMethods(source, methods);

			source.Append("}\n\n}");
			return source.ToString();
		}

		void AddServerRpcMethods(StringBuilder source, List<RPCMethodInfo> methods) {
			foreach ( var method in methods ) {
				AddServerRpcMethod(source, method);
			}
		}

		void AddServerRpcMethod(StringBuilder source, RPCMethodInfo method) {
			source.Append($@"
		public void S_CallRPC_{method.MethodName}({GetServerMethodFullArgumentString(method)} bool callOnServer) {{
			if ( callOnServer) {{
				{method.MethodName}({GetServerMethodCallArgumentString(method)});
			}}
			using (ByteStreamFormatter formatter = new ByteStreamFormatter() ) {{
");

			foreach ( var arg in method.Parameters ) {
				source.AppendLine($"				formatter.{BinaryFormatterHelper.GetWriteLiteral(arg.Type, arg.Identifier.ToString())};");
			}

			source.Append($@"
				ServerEntityController.Instance.CallRPCOnClient(this, {method.AttributeValue}, formatter.GetData());
			}}
		}}");
		}

		void AddSyncVarCompareField(StringBuilder source, SyncVarInfo syncVar) {
			source.AppendLine($"		private {syncVar.VarType} __sv_{syncVar.VarName};");
		}

		void AddSyncVarInit(StringBuilder source, List<SyncVarInfo> syncVars) {
			if ( syncVars.Count == 0 ) {
				return;
			}
			source.AppendLine("		");
			source.AppendLine("		protected override void InitSyncVars() {");
			source.AppendLine("			base.InitSyncVars();");
			foreach ( var sv in syncVars ) {
				source.AppendLine($"			__sv_{sv.VarName} = {sv.VarName};");
			}
			source.AppendLine("		}");
		}

		void AddSyncVarCheckMethod(StringBuilder source, List<SyncVarInfo> syncVars) {
			if ( syncVars.Count == 0 ) {
				return;
			}
			source.AppendLine("		");
			source.AppendLine("		protected override void CheckSyncVars() {");
			source.AppendLine("			base.CheckSyncVars();");
			source.AppendLine("			if ( !IsServer ) { return; }");
			foreach ( var syncVar in syncVars ) {
				AddSyncVarCheck(source, syncVar);
			}
			source.AppendLine("		}");
		}

		void AddSyncVarCheck(StringBuilder source, SyncVarInfo sv) {
			source.AppendLine($"			if ( __sv_{sv.VarName} != {sv.VarName} ) {{");
			source.AppendLine($"				using (ByteStreamFormatter formatter = new ByteStreamFormatter() ) {{");
			source.AppendLine($"					formatter.{BinaryFormatterHelper.GetWriteLiteral(sv.VarType, sv.VarName.ToString())};");
			source.AppendLine($"					ServerEntityController.Instance.SendSyncVarUpdate(this, {sv.AttributeValue}, formatter.GetData());");
			source.AppendLine($"					__sv_{sv.VarName} = {sv.VarName};");
			source.AppendLine($"				}}");
			source.AppendLine($"			}}");
		}

		void AddSyncVarApplyMethod(StringBuilder source, List<SyncVarInfo> syncVars) {
			if ( syncVars.Count == 0 ) {
				return;
			}
			source.AppendLine("		");
			source.AppendLine("		public override void Client_UpdateSyncVar(byte id, byte[] args) {");
			source.AppendLine("			base.Client_UpdateSyncVar(id, args);");
			source.AppendLine("			using ( ByteStreamFormatter reader = new ByteStreamFormatter(args) ) {");
			source.AppendLine("				switch ( id ) {");
			foreach ( var syncVar in syncVars ) {
				AddSyncVarApplyCaseBlock(source, syncVar);
			}
			source.AppendLine("					default: break;");
			source.AppendLine("				}");
			source.AppendLine("			}");
			source.AppendLine("		}");
		}

		void AddSyncVarApplyCaseBlock(StringBuilder source, SyncVarInfo sv) {
			source.AppendLine($"					case {sv.AttributeValue}:");
			source.AppendLine($"						{sv.VarName} = reader.{BinaryFormatterHelper.GetReadLiteral(sv.VarType)};");
			source.AppendLine($"						break;");
		}

		void AddUsings(StringBuilder source, List<RPCMethodInfo> methods, List<SyncVarInfo> syncVars) {
			List<UsingDirectiveSyntax> usings = methods.Count > 0 ? methods[0].Usings : syncVars[0].Usings;

			foreach ( var u in usings ) {
				source.AppendLine($"using {u.Name};");
			}
		}

		string GetNamespace(List<RPCMethodInfo> methods, List<SyncVarInfo> syncVars) {
			return (methods.Count > 0 ? methods[0].Namespace: syncVars[0].Namespace).ToString();
		}

		string GetServerMethodCallArgumentString(RPCMethodInfo method) {
			var paramsCount = method?.Parameters.Count() ?? 0;
			if ( method == null || paramsCount == 0 ) {
				return string.Empty;
			}
			var index = 0;
			var sb = new StringBuilder(64);
			foreach ( var arg in method.Parameters ) {
				sb.Append($" {arg.Identifier}");
				if ( index < paramsCount - 1) {
					sb.Append(", ");
				}
				index++;
			}
			return sb.ToString();
		}

		string GetServerMethodFullArgumentString(RPCMethodInfo method) {
			if ( method == null || method.Parameters.Count() == 0 ) {
				return string.Empty;
			}
			var sb = new StringBuilder(64);
			foreach ( var arg in method.Parameters ) {
				sb.Append($"{arg.Type} {arg.Identifier},");
			}
			return sb.ToString();
		}

		void AddClientRpcMethods(StringBuilder source, List<RPCMethodInfo> methods) {
			source.Append($@"
		public override void Client_CallRPC(byte id, byte[] args) {{
			base.Client_CallRPC(id, args);
			using ( ByteStreamFormatter reader = new ByteStreamFormatter(args) ) {{
			switch ( id ) {{
");
			foreach ( var method in methods ) {
				AddClientCallSwitchCase(source, method);
			}
			source.Append(@"			default:
				break;
			}");

			source.Append("\n		}	\n\n	}");
		}

		void AddClientCallSwitchCase(StringBuilder source, RPCMethodInfo methodInfo) {
			source.Append($@"			case {methodInfo.AttributeValue}:
				{methodInfo.MethodName}(");

			if (methodInfo.Parameters.Count() > 0) {
				var parIndex = 0;
				foreach ( var parameter in methodInfo.Parameters ) {
					if ( parIndex > 0 ) {
						source.Append( ", ");
					}
					var parType = parameter.Type;
					source.Append("reader.");
					source.Append(BinaryFormatterHelper.GetReadLiteral(parType));
					parIndex++;
				}
			}

			source.Append(@");
				break;
");
		}
	}

	internal class RPCMethodInfo {
		public ClassDeclarationSyntax ClassName;
		public SyntaxToken MethodName;
		public NameSyntax Namespace;
		public IEnumerable<ParameterSyntax> Parameters;
		public int AttributeValue;
		public List<UsingDirectiveSyntax> Usings = new List<UsingDirectiveSyntax>();
	}

	internal class SyncVarInfo {
		public ClassDeclarationSyntax ClassName;
		public SyntaxToken VarName;
		public NameSyntax Namespace;
		public int AttributeValue;
		public TypeSyntax VarType;
		public List<UsingDirectiveSyntax> Usings = new List<UsingDirectiveSyntax>();
	}

	internal class SyntaxReceiver : ISyntaxContextReceiver {
		public List<RPCMethodInfo> MethodInfos { get; }  = new List<RPCMethodInfo>();
		public List<SyncVarInfo>   SyncVarInfos { get; } = new List<SyncVarInfo>();

		public void OnVisitSyntaxNode(GeneratorSyntaxContext context) {
			CheckForClientRPCAttribute(context);
			CheckFieldForSyncVarAttribute(context);
			CheckPropertyForSyncVarAttribute(context);			
		}

		void CheckFieldForSyncVarAttribute(GeneratorSyntaxContext context) {
			if ( !(context.Node is FieldDeclarationSyntax fieldDecl) || fieldDecl.AttributeLists.Count == 0 ) {
				return;
			}
			var argVal = -1;
			foreach ( var attrList in fieldDecl.AttributeLists ) {
				foreach ( var attr in attrList.Attributes ) {
					if ( attr.Name.ToString() == "SyncVar" ) {
						if ( int.TryParse(attr.ArgumentList.Arguments.ToString(), out argVal) ) {
							break;
						}
					}
				}
			}
			if ( argVal < 0 ) {
				return;
			}
			var classDecl = (ClassDeclarationSyntax)fieldDecl.Parent;
			if ( classDecl == null ) {
				return;
			}
			var syncVarInfo = new SyncVarInfo();
			syncVarInfo.ClassName = classDecl;
			syncVarInfo.AttributeValue = argVal;
			syncVarInfo.VarName = fieldDecl.Declaration.Variables[0].Identifier;
			syncVarInfo.VarType = fieldDecl.Declaration.Type;
			syncVarInfo.Usings = CollectUsings(classDecl);
			var namespaceDecl = (NamespaceDeclarationSyntax)classDecl.Parent;
			if ( namespaceDecl != null ) {
				syncVarInfo.Namespace = namespaceDecl.Name;
			}
			SyncVarInfos.Add(syncVarInfo);
		}

		void CheckPropertyForSyncVarAttribute(GeneratorSyntaxContext context) {
			if ( !(context.Node is PropertyDeclarationSyntax propDecl) || propDecl.AttributeLists.Count == 0 ) {
				return;
			}
			var argVal = -1;
			foreach ( var attrList in propDecl.AttributeLists ) {
				foreach ( var attr in attrList.Attributes ) {
					if ( attr.Name.ToString() == "SyncVar" ) {
						if ( int.TryParse(attr.ArgumentList.Arguments.ToString(), out argVal) ) {
							break;
						}
					}
				}
			}
			if ( argVal < 0 ) {
				return;
			}
			var classDecl = (ClassDeclarationSyntax)propDecl.Parent;
			if ( classDecl == null ) {
				return;
			}
			var syncVarInfo = new SyncVarInfo();
			syncVarInfo.ClassName = classDecl;
			syncVarInfo.AttributeValue = argVal;
			syncVarInfo.VarName = propDecl.Identifier;
			syncVarInfo.VarType = propDecl.Type;
			syncVarInfo.Usings = CollectUsings(classDecl);
			var namespaceDecl = (NamespaceDeclarationSyntax)classDecl.Parent;
			if ( namespaceDecl != null ) {
				syncVarInfo.Namespace = namespaceDecl.Name;
			}
			SyncVarInfos.Add(syncVarInfo);
		}

		void CheckForClientRPCAttribute(GeneratorSyntaxContext context) {
			if ( !(context.Node is MethodDeclarationSyntax methodDecl) || methodDecl.AttributeLists.Count == 0) {
				return;
			}
			var argVal = -1;
			foreach ( var attrList in methodDecl.AttributeLists ) {
				foreach ( var attr in attrList.Attributes ) {
					if ( attr.Name.ToString() == "ClientRPC" ) {
						if ( int.TryParse(attr.ArgumentList.Arguments.ToString(), out argVal) ) {
							break;
						}
					}
				}
			}
			if ( argVal < 0 ) {
				return;
			}
			var classDecl = (ClassDeclarationSyntax)methodDecl.Parent;
			if ( classDecl == null ) {
				return;
			}

			var methodInfo = new RPCMethodInfo();
			methodInfo.ClassName = classDecl;
			methodInfo.AttributeValue = argVal;
			methodInfo.Parameters = methodDecl.ParameterList.Parameters;
			methodInfo.MethodName = methodDecl.Identifier;
			
			var namespaceDecl = (NamespaceDeclarationSyntax)classDecl.Parent;
			if ( namespaceDecl != null ) {
				methodInfo.Namespace = namespaceDecl.Name;
			}

			methodInfo.Usings = CollectUsings(classDecl);
			MethodInfos.Add(methodInfo);
		}

		List<UsingDirectiveSyntax> CollectUsings(ClassDeclarationSyntax classDecl) {
			var result = new List<UsingDirectiveSyntax>();
			var namespaceDecl = (NamespaceDeclarationSyntax)classDecl.Parent;
			var parentNode = (namespaceDecl != null ? namespaceDecl.Parent : classDecl.Parent).ChildNodes();
			foreach ( var node in parentNode ) {
				if ( node is UsingDirectiveSyntax ud ) {
					result.Add(ud);
				}
			}
			return result;
		}

		private bool IsDerivedFrom(INamedTypeSymbol baseType, string targetType) {
			while ( baseType != null ) {
				if ( baseType.Name == targetType )
					return true;

				baseType = baseType.BaseType;
			}

			return false;
		}
	}
}

/*
		foreach ( var item in classDecl.Modifiers ) {
			if ( item.ToString() == "abstract" ) {
				return;
			}
		}
*/